package fons.navigator.views.quadrant;

import java.awt.Font;
import java.awt.Stroke;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.apache.log4j.Logger;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.nodes.PPath;
import edu.umd.cs.piccolox.util.PFixedWidthStroke;
import fons.model.ontology.Colors;
import fons.model.ontology.Context;
import fons.model.ontology.Dimension;
import fons.model.ontology.Filters;
import fons.model.ontology.SelectionListener;
import fons.model.ontology.Value;
import fons.model.ontology.ValueSpace;
import fons.navigator.util.box2d.DynamicallyPlaceable;
import fons.navigator.util.piccolo.PPaths;

public class Spiral extends PNode implements SelectionListener {
	private static final long serialVersionUID = 1L;
	private static final Logger logger = Logger.getLogger(Spiral.class);
	
	public static final Font spiralDescriptionFont = new Font("Arial", Font.BOLD, 20);
	private static final Stroke ARROW_STROKE = new PFixedWidthStroke(4);
	
	public static class ValuePair {
		private final Value x;
		private final Value y;
		
		public ValuePair (Value x, Value y) {
			this.x = x;
			this.y = y;
		}
		
		public Value getX() {
			return x;
		}
		
		public Value getY() {
			return y;
		}
		
		@Override
		public String toString() {
			return "ValuePair [x=" + x + ", y=" + y + "]";
		}
		
		public boolean isValid() {
			return Context.missingValue != x && Context.missingValue != y;
		}
		
		public Point2D toPoint2D() {
			return new Point2D.Double(x.getValue(), y.getValue());
		}
		
		private PNode dotNode;
	}
	
	private final Dimension target;
	private final VisualSpiral visualSpiral;
	
	private final Map<Dimension, ValuePair> values = new HashMap<Dimension, ValuePair>();
	private final List<PPath> arrows;
	
	public Iterable<ValuePair> getValuePairs() {
		return values.values();
	}
	
	private double minX = Double.MAX_VALUE;
	private double maxX = Double.MIN_VALUE;
	private double minY = Double.MAX_VALUE;
	private double maxY = Double.MIN_VALUE;
	private double avgX;
	private double avgY;

	private boolean selected = false;
	private boolean valid = false;
	private final List<SpiralDescription> descriptions = new Vector<SpiralDescription>();
	private final List<Blocker> blockers = new Vector<Blocker>();
	
	public Spiral(final Dimension target, final ValueSpace targetSpace, final VisualSpiral visualSpiral) {

		this.target = target;
		this.visualSpiral = visualSpiral;
		this.arrows = new Vector<PPath>();
				
		createValuePoints(targetSpace);
	
		this.valid = values.size() != 0;
		if (values.size() == 0) return;
				
		createValuePointGraphics();		
		calculateStats();
		
		this.setSelected(false);

		visualSpiral.getContext().getStandardSelection()
		.addSelectionListener(target, this);

		this.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				super.mouseClicked(event);
				
				if (event.getClickCount() == 1) {
					if (Spiral.this.isSelected()) {
						visualSpiral.getContext().getStandardSelection().unselect("TARGETS", target);
						
					} else {
						visualSpiral.getContext().getStandardSelection().select("TARGETS", target);
					}
				}
			}
			
			@Override
			public void mouseEntered(PInputEvent event) {
				visualSpiral.showTooltip(Spiral.this, event.getPosition());
			}
			
			@Override
			public void mouseExited(PInputEvent event) {
				visualSpiral.hideTooltip();
			}
		});
	}

	private void createValuePointGraphics() {
		PNode firstDot = null;
		for (final Dimension valueDimension: values.keySet()) {
			final ValuePair valuePair = values.get(valueDimension);
			
			if (valuePair.isValid()) {

				valuePair.dotNode = new PNode();
				addChild(valuePair.dotNode);

				if (firstDot == null) firstDot = valuePair.dotNode;

				PPath dot = new PPath();
				dot.append(new Rectangle2D.Double(-3, -3, 6, 6), false);
				dot.setPaint(Colors.getColorFor(target, "node"));
				dot.setStroke(null);
				dot.rotate(Math.toRadians(45));
				valuePair.dotNode.addChild(dot);

				final SpiralDescription sd = new SpiralDescription(this, valueDimension.getName(), 1.0);
				valuePair.dotNode.addChild(sd);
				this.descriptions.add(sd);
				sd.setVisible(false);

				final Blocker blocker = new Blocker(10);
				valuePair.dotNode.addChild(blocker);
				this.blockers.add(blocker);
			}
		}
		
		SpiralDescription sd = new SpiralDescription(this, getLabel(), 0.1);
		sd.setFont(spiralDescriptionFont);
		this.descriptions.add(sd);
		
		if (firstDot != null)
			firstDot.addChild(sd);
	}

	private void createValuePoints(final ValueSpace targetSpace) {
		for (Dimension valueDimension: visualSpiral.getValueDimensions()) {
			final ValueSpace pointSpace = targetSpace.subSpace(valueDimension);
			
			Collection<Value> x = pointSpace.filter(Filters.allOf(visualSpiral.getXFilters()));
			Collection<Value> y = pointSpace.filter(Filters.allOf(visualSpiral.getYFilters()));
			if (x.size() == 1 && y.size() == 1) {
				final Value xv = x.iterator().next();
				final Value yv = y.iterator().next();
				
				if (xv != Context.missingValue && yv != Context.missingValue) {
					final ValuePair point = new ValuePair(xv, yv);
					values.put(valueDimension, point);
				}
			} else if (x.size() > 1 || y.size() > 1) {
				StringBuilder msg = new StringBuilder();
				msg.append("Problem: " + x.size() + " Werte f&uuml;r " + target.getName() +
						" und " + valueDimension.getName() + " gefunden. X-Achsen-Filter: ");
				for (Dimension filter: visualSpiral.getXFilters()) {
					msg.append(filter.getName() + "<br/>");
				}
				
				msg.append(y.size() + " Werte f&uuml;r " + target.getName() +
						" und " + valueDimension.getName() + " gefunden. Y-Achsen-Filter: ");
				for (Dimension filter: visualSpiral.getYFilters()) {
					msg.append(filter.getName() + " - ");
				}
				
				logger.warn(msg.toString());
			}
		}
	}

	private void calculateStats() {
		int counterX = 0;
		int counterY = 0;
				
		avgX = 0;
		avgY = 0;
				
		for (final ValuePair valuePair: values.values()) {						
			
			if (valuePair.isValid()) {
				final double x = valuePair.x.getValue();
				final double y = valuePair.y.getValue();
				
				if (x < minX)
					minX = x;
				if (x > maxX)
					maxX = x;
				
				avgX += x;
				counterX++;
				
				if (y < minY) 
					minY = y;
				if (y > maxY)
					maxY = y;
				
				avgY += y;
				counterY++;
			}
		}
		
		avgX /= counterX;
		avgY /= counterY;
	}
	
	public double getAverageX () {
		return avgX;
	}
	
	public double getAverageY () {
		return avgY;
	}
	
	public double getMaxX () {
		return maxX;
	}
	
	public double getMaxY () {
		return maxY;
	}
	
	public double getMinX () {
		return minX;
	}
	
	public double getMinY () {
		return minY;
	}
	
	public void updatePositionInCamera(PCamera camera) {
		removeChildren(arrows);
		arrows.clear();
		
		this.setBounds(visualSpiral.xToProjection(minX-2), visualSpiral.yToProjection(minY-2), 
			visualSpiral.xToProjection(maxX-minX+5), visualSpiral.yToProjection(maxY-minY+5));
		
		Point2D lastPos = null;
		ValuePair lastValuePair = null;
				
		for (final Dimension valueDimension: visualSpiral.getValueDimensions()) {//visualSpiral.getGraph().getValueDimensions()) { 
			
			final ValuePair valuePair = values.get(valueDimension);
			if (valuePair == null || !valuePair.isValid()) continue;
			
			final PNode dot = valuePair.dotNode;
			
			Point2D pos = visualSpiral.newProjectionFrom(valuePair.toPoint2D());
			camera.viewToLocal(pos);
			
			dot.setGlobalTranslation(pos);
			if (camera.getBoundsReference().contains(pos)) {	
				dot.setVisible(true);
			} else {
				dot.setVisible(false);
			}

			if (lastValuePair != null) {
				lastPos = camera.viewToLocal(visualSpiral.newProjectionFrom(lastValuePair.toPoint2D()));

				if (camera.getBoundsReference().contains(pos) || camera.getBoundsReference().contains(lastPos)) {
					PPath arrow = new PPath();
					arrow.append(PPaths.createArrowShape(lastPos, pos, 10, 30), false);
					arrow.setStroke(ARROW_STROKE);
					arrow.setStrokePaint(Colors.getTransparentColorFor(target, "node"));
					addChild(arrow);
					arrows.add(arrow);
					arrow.moveToBack();
				}
			}
			
			lastValuePair = valuePair;
		}
	}

	public String getLabel() {
		return target.getName();
	}

	public Map<Dimension, ValuePair> getValues() {
		return Collections.unmodifiableMap(this.values);
	}

	public Dimension getTargetDimension() {
		return target;
	}
	
	public boolean isSelected() {
		return selected ;
	}
	
	private void setSelected (boolean selected) {
		this.selected = selected;
		
		for (SpiralDescription sd: this.descriptions) {
			sd.setVisible(selected);
		}
	}

	public List<SpiralDescription> getDescriptions() {
		return Collections.unmodifiableList(descriptions);
	}
	
	public List<DynamicallyPlaceable> getPlaceables() {
		List<DynamicallyPlaceable> result = new Vector<DynamicallyPlaceable>();
		result.addAll(descriptions);
		result.addAll(blockers);
		return result;
	}

	public VisualSpiral getVisualSpiral() {
		return this.visualSpiral;
	}

	@Override
	public void select(String category, Dimension dimension) {
		if ("TARGETS".equals(category)) {
			this.setSelected(true);
		}
	}

	@Override
	public void unselect(String category, Dimension dimension) {
		if ("TARGETS".equals(category)) {
			this.setSelected(false);
		}
	}

	@Override
	public void dispose() {
		visualSpiral.getContext().getStandardSelection().removeSelectionListener(target, this);		
	}
	
	public boolean isValid() {
		return this.valid;
	}
}
